package ualrcdg.server;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Random;

import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import ualrcdg.shared.CDGXMLHelper;
import ualrcdg.shared.XMLHelper;
import ualrcdg.shared.dto.Game;
import ualrcdg.shared.exceptions.CDGException;
import ualrcdg.shared.exceptions.CDGParseException;
import ualrcdg.shared.requests.FetchGameRequest;
import ualrcdg.shared.requests.GameListRequest;
import ualrcdg.shared.responses.FetchGameResponse;
import ualrcdg.shared.responses.GameHeader;
import ualrcdg.shared.responses.GameListResponse;

/**
 * Handles all direct file manipulation of Game files, the initial parsing of new Game files, and all "fetching" of such files as requested by CDG Clients. 
 */
public class FileServer
{
	private final Path rootPath;
	private final Path gamesPath;
	private final Path newGamesPath;
	
	private static final Random r = new Random( );

	private static final String mediaFilePrefix = "media-";
	private static final String encodedFilePrefix = "encod-";
	
	private static final String fileNamePattern = "[a-z0-9_-]+\\.[a-z0-9_-]+";
	
	private final HashMap< String, GameWrapper > games;
	private final HashMap< String, GameHeader > gameHeaders;
	
	/**
	 * Primary constructor. Initializes all class members as well as the {@link CDGXMLHelper} class.
	 * 
	 * @param rootFolder the name of the folder (relative to the current working directory) that contains all files and folders necessary for the CDG Server application
	 * @param gamesFolder the name of the folder (within the rootFolder) that contains the individual Game folders
	 * @param newGamesFolder the name of the folder (within the rootFolder) into which new games and all associated files are copied before being parsed, validated, and copied into the gamesFolder  
	 * @param dtdListing the name of the file (within the rootFolder) that lists the DTD associations to be made within the CDGEntityResolver used on parsing XML files
	 * @throws FileNotFoundException If the rootPath or gamesFolder directories cannot be located
	 * @throws CDGException If an error is encountered while parsing the existing games located in the gamesFolder
	 * @throws IOException If an IO error occurs
	 * @throws ParserConfigurationException If such an exception is thrown by the CDGXMLHelper initialization routine
	 */
	public FileServer( String rootFolder, String gamesFolder, String newGamesFolder, String dtdListing ) throws FileNotFoundException, CDGException, IOException, ParserConfigurationException
	{
		if( rootFolder == null || gamesFolder == null || newGamesFolder == null )
		{
			throw new NullPointerException( "Unable to create FileServer. Folders cannot be null." );
		}
		
		System.out.println( "FileServer(" + rootFolder + ", " + gamesFolder + ", " + newGamesFolder + ", " + dtdListing + ")" );
		
		rootPath = FileSystems.getDefault( ).getPath( rootFolder );
		if( !Files.isDirectory( rootPath ) )
		{
			throw new FileNotFoundException( "FileServer: Unable to create FileServer. rootFolder [" + rootFolder + "] does not exist." );
		}

		CDGXMLHelper.init( rootPath, dtdListing );
		
		games = new HashMap< String, GameWrapper >( );
		gameHeaders = new HashMap< String, GameHeader >( );
		
		gamesPath = rootPath.resolve( gamesFolder );
		if( !Files.isDirectory( gamesPath ) )
		{
			Files.createDirectories( gamesPath );
		}
		else
		{
			File[] gameFolders = gamesPath.toFile( ).listFiles( );
			
			for( File f : gameFolders )
			{
				if( f.isDirectory( ) )
				{
					Path gamePath = f.toPath( );
					if( gamePath.resolve( "game.xml" ).toFile( ).exists( ) )
					{
						String id = f.getName( );
						GameWrapper gameWrapper = new GameWrapper( CDGXMLHelper.parseGame( gamePath ), id, gamesPath );
						registerGame( gameWrapper );
					}
				}
			}
		}
		
		newGamesPath = rootPath.resolve( newGamesFolder );
		if( !Files.isDirectory( newGamesPath ) )
		{
			throw new FileNotFoundException( "FileServer: Unable to create FileServer. newGamesFolder [" + newGamesFolder + "] does not exist." );
		}
	}
	
	/**
	 * Parses a CDG Game stored in a specified folder. Ensures that the game's XML file is valid and encodes any media files for transmission to clients, and moves all associated files to the gamesFolder known to the FileServer
	 * 
	 * @param newGamefolder the path which identifies the directory containing the to-be-parsed game
	 * @return the GameWrapper instance that describes (and contains) the parsed game
	 * @throws CDGParseException If a media file associated with the game has an invalid name 
	 * @throws IOException If an IO error occurs during file reading, writing, or movement
	 */
	private GameWrapper parseGame( Path newGamefolder ) throws IOException, CDGException
	{
		Game game = CDGXMLHelper.parseGame( newGamefolder );
		
		String id = generateId( );
		
		GameWrapper gameWrapper = new GameWrapper( game, id, gamesPath );
		Path gamePath = gameWrapper.resourcePath;
		
		try
		{
			Files.createDirectories( gameWrapper.resourcePath );
		}
		catch( IOException e )
		{
			throw new IOException( "FileServer: Unable to process game: " + gameWrapper.game.getName( ) + " from folder " + newGamefolder.toString( ) + ": unable to create new game directory.", e );
		}
		
		try
		{
			Files.copy( newGamefolder.resolve( "game.xml" ), gameWrapper.resourcePath.resolve( "game.xml" ), StandardCopyOption.REPLACE_EXISTING );
		}
		catch( IOException e )
		{
			throw new IOException( "FileServer: Unable to process game: " + gameWrapper.game.getName( ) + " from folder " + newGamefolder.toString( ) + ": unable to move game.xml file.", e );
		}
		
		try
		{
			Files.copy( newGamefolder.resolve( "chunk.xml" ), gameWrapper.resourcePath.resolve( "chunk.xml" ), StandardCopyOption.REPLACE_EXISTING );
		}
		catch( IOException e )
		{
			throw new IOException( "FileServer: Unable to process game: " + gameWrapper.game.getName( ) + " from folder " + newGamefolder.toString( ) + ": unable to move chunk.xml file.", e );
		}
		
		// LATER validate files

		for( String mediaFileName : gameWrapper.game.getFileMediaList( ) )
		{
			if( !mediaFileName.matches( fileNamePattern ) )
			{
				throw new CDGParseException( "FileServer: Unable to process game: " + gameWrapper.game.getName( ) + " from folder " + newGamefolder.toString( ) + ": invalid media file name [" + mediaFileName + "]." );
			}
			
			String newMediaFileName = mediaFilePrefix + mediaFileName;
			Path newPath = gamePath.resolve( newMediaFileName );
			
			String encodedFileName = encodedFilePrefix + mediaFileName + ".xml";
			Path encodedPath = gamePath.resolve( encodedFileName );
			
			Path mediaFilePath = newGamefolder.resolve( mediaFileName );
			try
			{
				Files.copy( mediaFilePath, newPath, StandardCopyOption.REPLACE_EXISTING );
			}
			catch( IOException e )
			{
				throw new IOException( "FileServer: Unable to process game: " + gameWrapper.game.getName( ) + " from folder " + newGamefolder.toString( ) + ": unable to move file ]" + mediaFileName + "].", e );
			}

			try
			{
				CDGXMLHelper.encodeAndXMLWrapMediaFile( newPath.toFile( ), mediaFileName, encodedPath.toFile( ) );
			}
			catch( FileNotFoundException e )
			{
				throw new IOException( "FileServer: Unable to process game: " + gameWrapper.game.getName( ) + " from folder " + newGamefolder.toString( ) + ": unable to encode file [" + mediaFileName + "].", e );
			}
			catch( IOException e )
			{
				throw new IOException( "FileServer: Unable to process game: " + gameWrapper.game.getName( ) + " from folder " + newGamefolder.toString( ) + ": unable to write encoded file [" + mediaFileName + "].", e );
			}
		}
		
		return gameWrapper;
	}
	
	/**
	 * Generates a random id to be associated with a newly parsed game
	 * 
	 * @return a (currently unallocated) random id to be used to uniquely identify a new game 
	 */
	private String generateId( )
	{
		String id = null;
		do
		{
			id = Long.toHexString( r.nextLong( ) );
		} while( games.containsKey( id ) );
		return id;
	}
	
	/**
	 * Registers a CDG Game with this FileServer instance.
	 * 
	 * @param game the game to be made available to CDG Clients accessing this FileServer's associated CDG Clients
	 * @throws CDGException if the game's id is already being used to identify a currently-registered game
	 */
	synchronized private void registerGame( GameWrapper game ) throws CDGException
	{
		System.out.println( "FileServer: registering game " + game );
		
		if( games.containsKey( game.id ) )
		{
			throw new CDGException( "FileServer: unable to register game with id: " + game.id + ". ID is already assigned." );
		}
		
		games.put( game.id, game );
		gameHeaders.put( game.id, new GameHeader( game.id, game.game.getName( ), game.game.getDescription( ) ) );
	}
	
	/**
	 * Parses, validates, and registers a game with this FileServer instance
	 * 
	 * @param folderName the name of the folder (within newGamesFolder) that contains the to-be-registered game's descriptor and media files
	 * @return true if the game was successfully registered, false otherwise
	 * @throws CDGException If an error occurred while registering the game
	 * @throws IOException If an IO error occurred while registering the game 
	 */
	synchronized public boolean registerGame( String folderName ) throws IOException, CDGException
	{
		if( folderName == null )
		{
			throw new NullPointerException( "FileServer.registerGame: folderName argument cannot be null." );
		}
		
		GameWrapper sg = null;
		try
		{
			sg = parseGame( newGamesPath.resolve( folderName ) );
		}
		catch( CDGException | IOException e )
		{
			throw e;
		}
		
		registerGame( sg );
		
		return true;
	}
	
	/**
	 * Returns an XML response document that contains a set of descriptions that describe all games registered with this FileServer
	 * 
	 * @param doc the XML request document requesting that the game descriptions be transmitted
	 * @return an XML response document (parsable as a GameListResponse object) as described above
	 * @throws CDGParseException If an error occurs while parsing the request document
	 */
	public Document getGameList( Document doc ) throws CDGParseException
	{
		GameListRequest request = CDGXMLHelper.parseGameListRequestDocument( doc );
		
		return CDGXMLHelper.createGameListResponseDocument( new GameListResponse( request.requestId, Collections.unmodifiableCollection( gameHeaders.values( ) ) ) );
	}
	
	/**
	 * Collects and returns the set of all XML files necessary for playing a specified CDG Game, as well as a header file that lists the names of each of these documents
	 * 
	 * @param request the XML request document that contains the ID of the game to be fetched
	 * @return an ArrayList of all XML documents associated with the specified game (as well as the above-mentioned header file)
	 * @throws SAXException If an error occurs while parsing one of the to-be-fetched files
	 * @throws IOException CDGXMLHelper.createGameListResponseDocument( new GameListResponse( request.requestId, 
	 * @throws CDGParseException If an error occurs while parsing the request document
	 */
	//LATER fix consistency issues: decide what should be on a GameWrapper's file list
	public ArrayList< Document > fetchGame( Document request ) throws SAXException, IOException, CDGParseException
	{
		FetchGameRequest req = CDGXMLHelper.parseFetchGameRequestDocument( request );
		
		GameWrapper game = games.get( req.gameId );
		if( game == null )
		{
			return null;
		}

		ArrayList< Document > result = new ArrayList< Document >( );
		
		ArrayList< String > fileNames = new ArrayList< String >( );
		fileNames.add( "game.xml" );
		fileNames.add( "chunk.xml" );
		fileNames.addAll( game.game.getFileMediaList( ) );
		
		result.add( CDGXMLHelper.createFetchGameResponseDocument( new FetchGameResponse( req.requestId, fileNames ) ) );
		
		result.add( XMLHelper.parseDocumentFromFile( game.resourcePath.resolve( "game.xml" ).toFile( ) ) );
		result.add( XMLHelper.parseDocumentFromFile( game.resourcePath.resolve( "chunk.xml" ).toFile( ) ) );
		
		for( String s : fileNames )
		{
			if( !s.equals( "game.xml" ) && !s.equals( "chunk.xml" ) )
			{
				result.add( XMLHelper.parseDocumentFromFile( game.resourcePath.resolve( encodedFilePrefix + s + ".xml" ).toFile( ) ) );
			}
		}
		
		return result;
	}
	
	/**
	 * Retrieves a single file associated with a specified game
	 * 
	 * @param gameId the id of the game with which the desired file is associated
	 * @param fileName the name of the file to be retrieved
	 * @return an XML document containing the requested file if the file exists, null otherwise 
	 * @throws IOException If an error occurs while parsing the requested file
	 * @throws SAXException If an error occurs while parsing the requested file
	 */
	public Document getFile( String gameId, String fileName ) throws SAXException, IOException
	{
		GameWrapper game = games.get( gameId );
		if( game == null )
		{
			return null;
		}
		
		if( !game.game.getFileMediaList( ).contains( fileName ) )
		{
			return null;
		}
		
		if( fileName.equals( "chunk.xml" ) || fileName.equals( "game.xml" ) )
		{
			return XMLHelper.parseDocumentFromFile( game.resourcePath.resolve( fileName ).toFile( ) );
		}
		
		return XMLHelper.parseDocumentFromFile( game.resourcePath.resolve( encodedFilePrefix + fileName + ".xml" ).toFile( ) );
	}
}